//
//  IDDMemoryCacheManager.m
//  ComJia
//
//  Created by Aslan on 17/02/2017.
//  Copyright © 2017 ComJia. All rights reserved.
//

#import "IDDMemoryCacheManager.h"
#import "IDDMemoryCacheManager_Private.h"

#define kMemoryCacheLimit 1024

@implementation IDDMemoryCacheManager

@synthesize privateCacheDirectory = _privateCacheDirectory;
@synthesize publicCacheDirectory = _publicCacheDirectory;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _recentlyAccessedKeys = [NSMutableArray arrayWithCapacity:kMemoryCacheLimit];
        _privateCacheKeys = [NSMutableArray arrayWithCapacity:kMemoryCacheLimit];
        _publicCacheKeys = [NSMutableArray arrayWithCapacity:kMemoryCacheLimit];
        _memoryCache = [NSMutableDictionary dictionaryWithCapacity:kMemoryCacheLimit];
        _limit = kMemoryCacheLimit;
    }
    return self;
}

- (void)setPrivateCacheDirectory:(NSString *)privateCacheDirectory
{
    _privateCacheDirectory = [privateCacheDirectory copy];
}

- (void)setPublicCacheDirectory:(NSString *)publicCacheDirectory
{
    _publicCacheDirectory = [publicCacheDirectory copy];
}

- (IDDCacheEntity *)getEntityForKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    if (error) {
        *error = nil;
    }
    IDDCacheEntity *entity = _memoryCache[key];
    if (entity) {
        @synchronized(self) {
            if ([_recentlyAccessedKeys containsObject:key]) {
                [_recentlyAccessedKeys removeObject:key];
            }
            [_recentlyAccessedKeys insertObject:key atIndex:0];
        }
    }
    return entity;
}

- (IDDCacheEntity *)setEntity:(IDDCacheEntity *)entity error:(IDDError *__autoreleasing *)error
{
    if (error) {
        *error = nil;
    }
    entity.dirty = YES;
    entity.path = entity.path ? entity.path : @"";
    IDDCacheEntity *leastRecentlyUsedCacheEntity = nil;
    NSString *key = @"";
    if (entity.type == IDDCacheEntityTypePrivate) {
        key = [NSString stringWithFormat:@"%@:%@@%@", _privateCacheDirectory, entity.path, entity.key];
    } else if (entity.type == IDDCacheEntityTypePublic) {
        key = [NSString stringWithFormat:@"%@:%@@%@", _publicCacheDirectory, entity.path, entity.key];
    } else {
        key = @"";
    }
    if (!entity.cache) {
        [_memoryCache removeObjectForKey:key];
        return entity;
    }
    
    BOOL keyExists = NO;
    unsigned long long oldSize = 0;
    if (_memoryCache[key]) {
        oldSize = ((IDDCacheEntity *)_memoryCache[key]).size;
        keyExists = YES;
    }
    
    _memoryCache[key] = entity;
    unsigned long long size = 0;
    if ([entity.cache conformsToProtocol:@protocol(NSCoding)]) {
        @try {
            NSData *data = [NSKeyedArchiver archivedDataWithRootObject:entity.cache];
            size = data.length;
            entity.size = size;
        }
        @catch (NSException *exception) {
            // do nothing
# if DEBUG || TEST
            @throw exception;
# endif
        }
    }
    @synchronized(self) {
        NSMutableArray *keyArray = nil;
        if (entity.type == IDDCacheEntityTypePrivate) {
            _privateCacheSize += size - oldSize;
            keyArray = _privateCacheKeys;
        } else if (entity.type == IDDCacheEntityTypePublic) {
            _publicCacheSize += size - oldSize;
            keyArray = _publicCacheKeys;
        }
        if ([_recentlyAccessedKeys containsObject:key]) {
            [_recentlyAccessedKeys removeObject:key];
        }
        [_recentlyAccessedKeys insertObject:key atIndex:0];
        if (_recentlyAccessedKeys.count > _limit) {
            NSString *leastRecentlyUsedKey = [_recentlyAccessedKeys lastObject];
            leastRecentlyUsedCacheEntity = _memoryCache[leastRecentlyUsedKey];
            [_recentlyAccessedKeys removeLastObject];
            [_memoryCache removeObjectForKey:leastRecentlyUsedKey];
        }
        if (!keyExists) {
            [keyArray addObject:key];
            [keyArray sortUsingComparator:^NSComparisonResult (id obj1, id obj2) {
                return [obj1 compare:obj2];
            }];
        }
    }
    
    return leastRecentlyUsedCacheEntity;
}

- (unsigned long long)cacheSize
{
    unsigned long long cacheSize = 0;
    cacheSize = _privateCacheSize + _publicCacheSize;
    return cacheSize;
}

- (unsigned long long)privateCacheSizeAtPath:(NSString *)path
{
    unsigned long long cacheSize = 0;
    if (path.length) {
        for (NSString *key in _privateCacheKeys) {
            if ([key compare:path] == NSOrderedDescending) {
                break;
            }
            if ([key hasPrefix:path]) {
                cacheSize += ((IDDCacheEntity *)_memoryCache[key]).size;
            }
        }
    } else {
        cacheSize = _privateCacheSize;
    }
    return cacheSize;
}

- (unsigned long long)publicCacheSizeAtPath:(NSString *)path
{
    unsigned long long cacheSize = 0;
    if (path.length) {
        for (NSString *key in _publicCacheKeys) {
            if ([key compare:path] == NSOrderedDescending) {
                break;
            }
            if ([key hasPrefix:path]) {
                cacheSize += ((IDDCacheEntity *)_memoryCache[key]).size;
            }
        }
    } else {
        cacheSize = _publicCacheSize;
    }
    return cacheSize;
}

- (void)cleanCache
{
    [_memoryCache removeAllObjects];
    [_recentlyAccessedKeys removeAllObjects];
    [_privateCacheKeys removeAllObjects];
    [_publicCacheKeys removeAllObjects];
    _privateCacheSize = 0;
    _publicCacheSize = 0;
}

- (void)cleanPrivateCacheAtPath:(NSString *)path
{
    NSArray *toBeDeletedKeys = nil;
    NSMutableArray *array = [NSMutableArray array];
    unsigned long long size = 0;
    if (path.length) {
        path = [NSString stringWithFormat:@"%@:%@", _privateCacheDirectory, path];
        for (NSString *key in _privateCacheKeys) {
            NSString *keyPath = [key substringToIndex:[key rangeOfString:@"@"].location];
            if ([keyPath compare:path] == NSOrderedDescending) {
                break;
            }
            if ([key hasPrefix:path]) {
                [array addObject:key];
                size += ((IDDCacheEntity *)_memoryCache[key]).size;
            }
        }
    } else {
        array = _privateCacheKeys;
        size = _privateCacheSize;
    }
    toBeDeletedKeys = [array copy];
    
    [_memoryCache removeObjectsForKeys:toBeDeletedKeys];
    [_recentlyAccessedKeys removeObjectsInArray:toBeDeletedKeys];
    [_privateCacheKeys removeObjectsInArray:toBeDeletedKeys];
    _privateCacheSize -= size;
}

- (void)cleanPublicCacheAtPath:(NSString *)path
{
    NSArray *toBeDeletedKeys = nil;
    NSMutableArray *array = [NSMutableArray array];
    unsigned long long size = 0;
    if (path.length) {
        path = [NSString stringWithFormat:@"%@:%@", _publicCacheDirectory, path];
        for (NSString *key in _publicCacheKeys) {
            NSString *keyPath = [key substringToIndex:[key rangeOfString:@"@"].location];
            if ([keyPath compare:path] == NSOrderedDescending) {
                break;
            }
            if ([key hasPrefix:path]) {
                [array addObject:key];
                size += ((IDDCacheEntity *)_memoryCache[key]).size;
            }
        }
    } else {
        array = _publicCacheKeys;
        size = _publicCacheSize;
    }
    toBeDeletedKeys = [array copy];
    
    [_memoryCache removeObjectsForKeys:toBeDeletedKeys];
    [_recentlyAccessedKeys removeObjectsInArray:toBeDeletedKeys];
    [_publicCacheKeys removeObjectsInArray:toBeDeletedKeys];
    _publicCacheSize -= size;
}

- (IDDCacheEntity *)privateCacheForKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self privateCacheForKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)privateCacheForKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    IDDError *thisError = nil;
    path = path ? path : @"";
    key = [NSString stringWithFormat:@"%@:%@@%@", _privateCacheDirectory, path, key];
    IDDCacheEntity *entity = [self getEntityForKey:key error:&thisError];
    if (thisError) {
        if (error) {
            *error = thisError;
        }
        return nil;
    }
    return entity;
}

- (void)privateCacheForKey:(NSString *)key finished:(IDDCacheManagerCallback)finished
{
    [self privateCacheForKey:key atPath:nil finished:finished];
}

- (void)privateCacheForKey:(NSString *)key atPath:(NSString *)path finished:(IDDCacheManagerCallback)finished
{
    IDDError *thisError = nil;
    path = path ? path : @"";
    key = [NSString stringWithFormat:@"%@:%@@%@", _privateCacheDirectory, path, key];
    IDDCacheEntity *entity = [self getEntityForKey:key error:&thisError];
    if (thisError) {
        finished(nil, thisError);
    } else {
        finished(entity, nil);
    }
}

- (IDDCacheEntity *)setPrivateCache:(id)cache forKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self setPrivateCache:cache forKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)setPrivateCache:(id)cache forKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    IDDError *thisError = nil;
    IDDCacheEntity *entity = [[IDDCacheEntity alloc] initWithCache:cache forKey:key atPath:path andType:IDDCacheEntityTypePrivate];
    IDDCacheEntity *staleEntity = [self setEntity:entity error:&thisError];
    if (thisError) {
        if (error) {
            *error = thisError;
        }
        return nil;
    }
    return staleEntity;
}

- (IDDCacheEntity *)publicCacheForKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self publicCacheForKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)publicCacheForKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    IDDError *thisError = nil;
    path = path ? path : @"";
    key = [NSString stringWithFormat:@"%@:%@@%@", _publicCacheDirectory, path, key];
    IDDCacheEntity *entity = [self getEntityForKey:key error:&thisError];
    if (thisError) {
        if (error) {
            *error = thisError;
        }
        return nil;
    }
    return entity;
}

- (void)publicCacheForKey:(NSString *)key finished:(IDDCacheManagerCallback)finished
{
    [self publicCacheForKey:key atPath:nil finished:finished];
}

- (void)publicCacheForKey:(NSString *)key atPath:(NSString *)path finished:(IDDCacheManagerCallback)finished
{
    IDDError *thisError = nil;
    path = path ? path : @"";
    key = [NSString stringWithFormat:@"%@:%@@%@", _publicCacheDirectory, path, key];
    IDDCacheEntity *entity = [self getEntityForKey:key error:&thisError];
    if (thisError) {
        finished(nil, thisError);
    } else {
        finished(entity, nil);
    }
}

- (IDDCacheEntity *)setPublicCache:(id)cache forKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self setPublicCache:cache forKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)setPublicCache:(id)cache forKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    IDDError *thisError = nil;
    IDDCacheEntity *entity = [[IDDCacheEntity alloc] initWithCache:cache forKey:key atPath:path andType:IDDCacheEntityTypePublic];
    IDDCacheEntity *staleEntity = [self setEntity:entity error:&thisError];
    if (thisError) {
        if (error) {
            *error = thisError;
        }
        return nil;
    }
    return staleEntity;
}

@end
